use crate::id::Id;
use proto_pdk_api::{Checksum, ToolLockOptions};
use serde::{Deserialize, Serialize};
use starbase_utils::fs;
use starbase_utils::toml::{self, TomlError};
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use system_env::{SystemArch, SystemOS};
use tracing::{debug, instrument};
use version_spec::{UnresolvedVersionSpec, VersionSpec};

pub const PROTO_LOCK_NAME: &str = ".protolock";

#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(default)]
pub struct LockRecord {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub os: Option<SystemOS>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub arch: Option<SystemArch>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub backend: Option<Id>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub spec: Option<UnresolvedVersionSpec>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub version: Option<VersionSpec>,

    // Build from source and native installs may not have a checksum
    #[serde(skip_serializing_if = "Option::is_none")]
    pub checksum: Option<Checksum>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub source: Option<String>,
}

impl LockRecord {
    pub fn for_manifest(&self) -> Self {
        let mut record = self.clone();
        record.spec = None;
        record.version = None;
        record
    }

    pub fn for_lockfile(&self) -> Self {
        let mut record = self.clone();
        record.source = None;
        record
    }

    pub fn is_match(&self, other: &Self, options: &ToolLockOptions) -> bool {
        self.is_match_with(
            other.backend.as_ref(),
            other.spec.as_ref(),
            other.os.as_ref(),
            other.arch.as_ref(),
            options,
        )
    }

    pub fn is_match_with(
        &self,
        backend: Option<&Id>,
        spec: Option<&UnresolvedVersionSpec>,
        os: Option<&SystemOS>,
        arch: Option<&SystemArch>,
        options: &ToolLockOptions,
    ) -> bool {
        if self.backend.as_ref() != backend || self.spec.as_ref() != spec {
            return false;
        }

        if options.ignore_os_arch {
            // If the tool is ignoring os/arch but this record (in the lockfile)
            // has an os/arch, then it shouldn't match
            if self.os.is_some() || self.arch.is_some() {
                return false;
            }
        } else {
            // If thet tool is matching os/arch, then we need to ensure that this
            // record (in the lockfile) matches the values, except for none,
            // as none entries exist for backwards compatibility
            if self.os.is_some() && self.os.as_ref() != os
                || self.arch.is_some() && self.arch.as_ref() != arch
            {
                return false;
            }
        }

        true
    }
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct ProtoLock {
    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
    pub tools: BTreeMap<Id, Vec<LockRecord>>,

    #[serde(skip)]
    pub path: PathBuf,
}

impl ProtoLock {
    pub fn load_from<P: AsRef<Path>>(dir: P) -> Result<Self, TomlError> {
        Self::load(Self::resolve_path(dir))
    }

    #[instrument(name = "load_lock")]
    pub fn load<P: AsRef<Path> + Debug>(path: P) -> Result<Self, TomlError> {
        let path = path.as_ref();

        debug!(file = ?path, "Loading lock file");

        let mut manifest: ProtoLock = if path.exists() {
            toml::read_file(path)?
        } else {
            ProtoLock::default()
        };

        manifest.path = path.into();

        Ok(manifest)
    }

    #[instrument(name = "save_lock", skip(self))]
    pub fn save(&self) -> Result<(), TomlError> {
        if self.tools.is_empty() {
            debug!(file = ?self.path, "Removing lock file because its empty");

            fs::remove_file(&self.path)?;

            return Ok(());
        }

        debug!(file = ?self.path, "Saving lock file");

        let content = toml::format(self, true)?;

        fs::write_file(
            &self.path,
            format!("# Generated by proto. Do not modify!\n\n{content}"),
        )?;

        Ok(())
    }

    pub fn sort_records(&mut self) {
        for records in self.tools.values_mut() {
            records.sort_by_key(|record| {
                (
                    record.spec.clone(),
                    record.backend.clone(),
                    record.os,
                    record.arch,
                )
            });
        }
    }

    fn resolve_path(path: impl AsRef<Path>) -> PathBuf {
        let path = path.as_ref();

        if path.ends_with(PROTO_LOCK_NAME) {
            path.to_path_buf()
        } else {
            path.join(PROTO_LOCK_NAME)
        }
    }
}
